/***************************************************************************
 *   Copyright (C) 2007 by Ziman & Xloverin                                *
 *   ziman@centrum.sk, xloverin@gmail.com                                  *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/

#include <QByteArray>
#include <QScrollBar>
#include <QMimeData>
#include <QMenu>
#include <QFileDialog>
#include <QDebug>
#include <QFile>
#include <QTextStream>
#include <QDesktopWidget>
#include <QClipboard>
#include <stdexcept>
#include <string>
#include "terminal.h"
#include "query.h"

void Terminal::on_actionClear_triggered()
{
	clear();
}

QString Terminal::css()
{
	return
		" body { background-color: black; color: #ccc; font-family: '" + m_fontFace +
		"'; font-size: " + QString::number(m_fontSize) + "px }"
		" h1 { font-weight: bold; font-size: 1.5em }"
		" span.timestamp { color: #777 } "
		" span.u { text-decoration: underline }"
		" span.f8, span.f9, span.f10, span.f11, span.f12, "
		" 	span.f13, span.f14, span.f15 { font-weight: bold }"
		" span.f0 { color: #000 }"
		" span.f1 { color: #c00 }"
		" span.f2 { color: #0c0 }"
		" span.f3 { color: #cc0 }"
		" span.f4 { color: #00c }"
		" span.f5 { color: #c0c }"
		" span.f6 { color: #0cc }"
		" span.f7 { color: #ccc }"
		" span.f8 { color: #777 }"
		" span.f9 { color: #f00 }"
		" span.f10 { color: #0f0 }"
		" span.f11 { color: #ff0 }"
		" span.f12 { color: #00f }"
		" span.f13 { color: #f0f }"
		" span.f14 { color: #0ff }"
		" span.f15 { color: #fff }"
		" span.b0 { background: #000 }"
		" span.b1 { background: #800 }"
		" span.b2 { background: #080 }"
		" span.b3 { background: #880 }"
		" span.b4 { background: #008 }"
		" span.b5 { background: #808 }"
		" span.b6 { background: #088 }"
		" span.b7 { background: #888 }";
}

void Terminal::setFont(QString face, int size)
{
	if (m_fontFace == face && m_fontSize == size) return;
	
	QString html = display->toHtml();
	html.replace(QRegExp("font-size:[^;\"]*"), "font-size: " + QString::number(size) + "px");
	html.replace(QRegExp("font-family:[^;\"]*"), "font-family:'" + face + "'");
	display->setHtml(html);
	
	display->verticalScrollBar()->setSliderPosition(display->verticalScrollBar()->maximum());
	
	m_fontFace = face;
	m_fontSize = size;
}

Query *Terminal::query()
{
	return m_query;
}

void Terminal::setQuery(Query *query)
{
	m_query = query;
}

void Terminal::closeTerminal()
{
	m_reallyClose = true;
	setAttribute(Qt::WA_DeleteOnClose);
	close();
}

void Terminal::closeEvent(QCloseEvent *evt)
{
	if (!m_reallyClose) {
		emit closeTab(this);
		if (!m_reallyClose) evt->ignore();
	}
}

void Terminal::on_actionClose_triggered()
{
	emit closeTab(this);
}

void Terminal::on_actionSave_as_HTML_triggered()
{
	QString fn = QFileDialog::getSaveFileName(this, tr("Save as HTML"), ".", tr("HTML documents (*.html)"));
	if (!fn.isEmpty()) {
		QFile f(fn);
		f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text);
		QTextStream stream(&f);
		stream << display->toHtml();
	}
}

void Terminal::on_actionReattach_triggered()
{
	if (!reattachWidget) return;
	reattach();
}

void Terminal::contextMenuEvent(QContextMenuEvent *evt)
{
	QMenu menu(this);
	menu.addAction(actionCopy);
	menu.addSeparator();
	menu.addAction(actionSave_as_HTML);
	menu.addAction(actionEnable_color);
	menu.addAction(actionClear);
	menu.addSeparator();
	menu.addAction(actionDetach);
	menu.addAction(actionReattach);
	menu.addSeparator();
	menu.addAction(actionClose);
	menu.exec(evt->globalPos());
}

void Terminal::reattach()
{
	actionReattach->setEnabled(false);
	actionDetach->setEnabled(true);
	reattachWidget->addTab(this, QIcon(Query::hlIcons[Query::Inactive]), windowTitle());
	
	emit attached();

	reattachWidget = 0;
}

void Terminal::detach(QString title, QTabWidget *reattachTo)
{
	if (parentWidget() == 0) {
		throw std::logic_error("Terminal::detach(): Parent is not a QWidget or is NULL.");
	}
	QRect rect = qApp->desktop()->screenGeometry();
	rect = QRect(
		(rect.width() - parentWidget()->width()) / 2,
		(rect.height() - parentWidget()->height()) / 2,
		parentWidget()->width(),
		parentWidget()->height()
	);
	
	reattachWidget = reattachTo;

	actionDetach->setEnabled(false);
	actionReattach->setEnabled(true);
	setParent(0);
	setWindowTitle(title);
	
	setGeometry(rect);	
	
	show();
}

void Terminal::focusInput()
{
	input->setFocus();
}

bool Terminal::color()
{
	return m_color;
}

void Terminal::setColor(bool color)
{
	m_color = color;
	actionEnable_color->setChecked(m_color);
}

void Terminal::setClosableFromContextMenu(bool closable)
{
	actionClose->setEnabled(closable);
}

void Terminal::on_display_selectionChanged()
{
	QClipboard *cb = QApplication::clipboard();
	cb->setText(display->textCursor().selectedText());
}

Terminal::Terminal()
{
	setupUi(this);

	m_reallyClose = false;
	m_color = true;
	m_query = 0;
	m_fontFace = "Courier New";
	m_fontSize = 10;
	reattachWidget = 0;
	historyPos = 0;

	clear();

	connect(actionEnable_color, SIGNAL(toggled(bool)), this, SLOT(setColor(bool)));
	connect(actionDetach, SIGNAL(triggered()), this, SIGNAL(detachTab()));
	connect(actionCopy, SIGNAL(triggered()), display, SLOT(copy()));
	connect(display, SIGNAL(selectionChanged()), this, SLOT(on_display_selectionChanged()));
}

Terminal::~Terminal()
{
}

void Terminal::echo(bool on)
{
	if (on) {
		input->setEchoMode(QLineEdit::Normal);
	} else {
		input->setEchoMode(QLineEdit::Password);
	}
}

#define EMIT(x) if (!cur) result += x
QString Terminal::interpretVt100(QByteArray data, Terminal::Output output, Terminal *terminal, QTextCursor *cur)
{
	static const QColor fgColors[16] = {
		QColor(0x00, 0x00, 0x00), QColor(0xCC, 0x00, 0x00),
		QColor(0x00, 0xCC, 0x00), QColor(0xCC, 0xCC, 0x00),
		QColor(0x00, 0x00, 0xCC), QColor(0xCC, 0x00, 0xCC),
		QColor(0x00, 0xCC, 0xCC), QColor(0xCC, 0xCC, 0xCC),
		QColor(0x77, 0x77, 0x77), QColor(0xFF, 0x00, 0x00),
		QColor(0x00, 0xFF, 0x00), QColor(0xFF, 0xFF, 0x00),
		QColor(0x00, 0x00, 0xFF), QColor(0xFF, 0x00, 0xFF),
		QColor(0x00, 0xFF, 0xFF), QColor(0xFF, 0xFF, 0xFF)
	};

	static const QColor bgColors[8] = {
		QColor(0x00, 0x00, 0x00), QColor(0x88, 0x00, 0x00),
		QColor(0x00, 0x88, 0x00), QColor(0x88, 0x88, 0x00),
		QColor(0x00, 0x00, 0x88), QColor(0x88, 0x00, 0x88),
		QColor(0x00, 0x88, 0x88), QColor(0x88, 0x88, 0x88)
	};



	QString result;

	int length = data.size();
	result.reserve(3 * length);

	// Font
	QTextCharFormat fmt;
	if (cur) fmt = cur->charFormat();

	enum State {sCopy, sColor};
	State state = sCopy;

	QString curColor;
	curColor.reserve(32);
	int bgColor = 7, fgColor = 0;
	bool underline = false;
	int depth = 0;
	QChar prevChar = ' ';
	
	char ch_str[2] = { '\0', '\0' };
	unsigned char &ch = (unsigned char &) ch_str[0];
	
	for (int i = 0; i < length; ++i) {
		ch = data.at(i);
		
		if (ch == 255 /* IAC */ ) {
			/* Telnet command */

			if (i < length - 2) {

				if (terminal == 0) {
					i += 2;
					continue;
				}

				unsigned char op = data.at(i+1);
				unsigned char action = data.at(i+2);
				
				if (action == 1 /* ECHO */) {
					switch (op) {
						case 252: /* WONT */
						case 253: /* DO */
							terminal->echo(true);
							break;

						case 254: /* DONT */
						case 251: /* WILL */
							terminal->echo(false);
							break;

						default:
							/* Ignore */
							break;
					}
				}

				i += 2;
				continue;
			} else {
				/* Ignorujeme nekompletny telnet prikaz */
			}
		}

		switch (state) {
			case sCopy:
				if (ch == '\e') {
					state = sColor;
					curColor = "";
					// prevChar bezo zmeny
					break;
				}

				if (output == PlainText) {
					EMIT(ch);
					if (cur) cur->insertText(QString::fromLocal8Bit(ch_str));
				} else {

					// Convert to HTML
					switch (ch) {
						case '<':
							EMIT("&lt;");
							if (cur) cur->insertText(QString(ch));
							break;

						case '>':
							EMIT("&gt;");
							if (cur) cur->insertText(QString(ch));
							break;

						case '&':
							EMIT("&amp;");
							if (cur) cur->insertText(QString(ch));
							break;

						case '\r':
							/* Ignore */
							break;

						case '\n':
							EMIT("<br/>\n");
							if (cur) cur->insertText(QString(ch));
							// Novy riadok
							break;

						case '\t':
							EMIT("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;");
							if (cur) cur->insertText("        ");
							break;

						case ' ':
							EMIT(prevChar.isSpace() ? "&nbsp;" : " ");
							if (cur) cur->insertText(QString(ch));
							break;

						default:
							EMIT(ch);
							if (cur) cur->insertText(QString(ch));
					}

					prevChar = ch;
				}

				break;

			case sColor:
				switch (ch) {
					case '[':
						/* Tuto zatvorku ignorujeme */
						break;
					case 'm':
						state = sCopy;
					case ';': {
						if (output != Html) break;

						bool ok;
						int code = curColor.toInt(&ok);
						if (ok) switch (code) {
							case 0:
								/* Reset */
								if (depth > 0) do {
									--depth;
									EMIT("</span>");
								} while (depth > 0);

								if (underline) {
									fmt.setFontUnderline(false);
									underline = false;
								}

								if (bgColor != 0) {
									fmt.setBackground(Qt::black);
									bgColor = 0;
								}

								if (fgColor != 7) {
									if (fgColor & 0x8) {
										/* Bold */
										fmt.setFontWeight(QFont::Normal);
									}
									fmt.setForeground(QColor(0xCC, 0xCC, 0xCC));
									fgColor = 7;
								}

								break;

							case 1:
								/* Bold */
								if ((fgColor & 8) == 0) {
									++depth;
									fgColor |= 0x8; // Bold is indicated by the third bit
									EMIT("<span class=\"f" + QString::number(fgColor) + "\">");
									fmt.setFontWeight(QFont::Bold);
									fmt.setForeground(fgColors[fgColor]);
								}
								break;

							case 4:
								/* Underline */
								if (!underline) {
									++depth;
									EMIT("<span class=\"u\">");
									fmt.setFontUnderline(true);
									underline = true;
								}
								break;

							case 30: case 31: case 32: case 33:
							case 34: case 35: case 36: case 37:
								/* Foreground color */
								{
									// Vypocitame novu farbu so zachovanim bold flagu
									int ncol = (code - 30) | (fgColor & 0x8);

									// Porovname so starou, ci sa ozaj zmenila
									if (fgColor != ncol) {
										/* Farba sa naozaj zmenila */
										fgColor = ncol;
										++depth;
										EMIT("<span class=\"f" + QString::number(fgColor) + "\">");

										fmt.setForeground(fgColors[fgColor]);
									}
								}
								break;

							case 40: case 41: case 42: case 43:
							case 44: case 45: case 46: case 47:
								/* Background color */
								if (bgColor != (code-40)) {
									bgColor = code - 40;
									++depth;
									EMIT("<span class=\"b" + QString::number(fgColor) + "\">");

									fmt.setBackground(bgColors[bgColor]);
								}
								break;
							default:
								/* Neznamy farebny kod, ignorujeme :-) */
								break;
						} else {
							/* Retazec udavajuci farebny kod bol neciselny - ignorujeme */
						}

						// Nastavime format QTextDocumentu
						if (cur) cur->setCharFormat(fmt);

						break;
					}

					default:
						curColor += ch;
				}
				break;
		}
	}

	// Uzavrieme vsetky tagy, ktore ostali
	if (depth > 0) do {
		--depth;
		EMIT("</span>");
	} while (depth > 0);


	return result;
}

void Terminal::clear()
{
	display->setHtml(
		"<html><head><title>term</title><style type=\"text/css\">"
		+ QString(css())
		+ "</style></head><body></body></html>"
	);
}

void Terminal::write(QByteArray line, bool stripTelnet)
{
	int sbPos = display->verticalScrollBar()->sliderPosition();
	if (display->verticalScrollBar()->sliderPosition() == display->verticalScrollBar()->maximum()) {
		sbPos = -1;
	}

	QTextCursor cur = display->textCursor();
	cur.movePosition(QTextCursor::End);
	interpretVt100(line, m_color ? Html : MonoHtml, stripTelnet ? 0 : this, &cur);

	display->verticalScrollBar()->setSliderPosition((sbPos == -1) ? display->verticalScrollBar()->maximum() : sbPos);
}

void Terminal::on_input_textEdited(const QString &text)
{
	// Neukladat heslo
	if (input->echoMode() == QLineEdit::Password) return;
	
	if (historyPos < inputHistory.count()) {
		/* Editujeme existujuci riadok v historii */
		inputHistory[historyPos] = text;
	} else {
		/* Editujeme este neexistujuci riadok */
		historyPos = inputHistory.count();
		inputHistory.append(text);
	}
}

bool Terminal::getEcho() const
{
	return (input->echoMode() == QLineEdit::Normal);
}

void Terminal::on_input_keyPressed(Qt::Key key)
{
	switch (key) {

		case Qt::Key_Up:
			if (historyPos > 0) --historyPos;
			input->setText(inputHistory[historyPos]);
			break;

		case Qt::Key_Down:
			if (historyPos >= inputHistory.count() - 1) {
				input->setText("");
				historyPos = inputHistory.count();
			} else {
				++historyPos;
				input->setText(inputHistory[historyPos]);
			}
			break;

		case Qt::Key_PageUp: {
			QScrollBar *sb = display->verticalScrollBar();
			sb->setSliderPosition(sb->sliderPosition() - sb->pageStep()/2);
			break;
		}

		case Qt::Key_PageDown: {
			QScrollBar *sb = display->verticalScrollBar();
			sb->setSliderPosition(sb->sliderPosition() + sb->pageStep()/2);
			break;
		}

		default:
			break;
	}
}

void stripDiacritics(QString &s)
{
	int len = s.length();
	for (int i = 0; i < len; ++i) {
		QString comp = s[i].decomposition();
		if (comp.length() > 0) s[i] = comp[0];
		char ch = s[i].toAscii();
		if (ch < ' ' && ch != '\r' && ch != '\n') {
			ch = ' ';
		}
		s[i] = ch;
	}
}

void Terminal::on_input_returnPressed()
{
	QString line = input->text();
	input->setText("");
		
	// Ak sme odoslali riadok z historie, tak ho skopirujeme ako posledny
	if (historyPos < inputHistory.count()-1) {
		inputHistory.append(line);
	}

	historyPos = inputHistory.count();
	
	stripDiacritics(line);
	line.replace('\n', ' ');
	emit lineSubmitted(line);
}


